<?php

/**
 * Class QRGdImage
 *
 * @created      05.12.2015
 *
 * @author       Smiley <smiley@chillerlan.net>
 * @copyright    2015 Smiley
 * @license      MIT
 *
 * @noinspection PhpComposerExtensionStubsInspection
 */
declare(strict_types=1);

namespace zxf\QrCode\Output;

use const IMG_AVIF;
use const IMG_BMP;
use const IMG_GIF;
use const IMG_JPG;
use const IMG_PNG;
use const IMG_WEBP;

use GdImage;
use zxf\QrCode\Data\QRMatrix;
use zxf\QrCode\QROptions;
use zxf\QrCode\Settings\SettingsContainerInterface;

use function extension_loaded;
use function imagecolorallocate;
use function imagecolortransparent;
use function imagecreatetruecolor;
use function imagedestroy;
use function imagefilledellipse;
use function imagefilledrectangle;
use function imagescale;
use function imagetypes;
use function intdiv;
use function intval;
use function max;
use function min;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function sprintf;

/**
 * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
 *
 * @see https://php.net/manual/book.image.php
 * @see https://github.com/chillerlan/php-qrcode/issues/223
 */
abstract class QRGdImage extends QROutputAbstract
{
    use RGBArrayModuleValueTrait;

    /**
     * The GD image resource
     *
     * @see imagecreatetruecolor()
     */
    protected GdImage $image;

    /**
     * The allocated background color
     *
     * @see \imagecolorallocate()
     */
    protected int $background;

    /**
     * Whether we're running in upscale mode (scale < 20)
     *
     * @see \zxf\QrCode\QROptions::$drawCircularModules
     */
    protected bool $upscaled = false;

    /**
     * @throws \zxf\QrCode\Output\QRCodeOutputException
     *
     * @noinspection PhpMissingParentConstructorInspection
     */
    public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix)
    {
        $this->options = $options;
        $this->matrix = $matrix;

        $this->checkGD();

        if ($this->options->invertMatrix) {
            $this->matrix->invert();
        }

        $this->copyVars();
        $this->setMatrixDimensions();
    }

    /**
     * Checks whether GD is installed and if the given mode is supported
     *
     * @throws \zxf\QrCode\Output\QRCodeOutputException
     *
     * @codeCoverageIgnore
     */
    protected function checkGD(): void
    {

        if (! extension_loaded('gd')) {
            throw new QRCodeOutputException('ext-gd not loaded');
        }

        $modes = [
            QRGdImageAVIF::class => IMG_AVIF,
            QRGdImageBMP::class => IMG_BMP,
            QRGdImageGIF::class => IMG_GIF,
            QRGdImageJPEG::class => IMG_JPG,
            QRGdImagePNG::class => IMG_PNG,
            QRGdImageWEBP::class => IMG_WEBP,
        ];

        // likely using custom output/manual invocation
        if (! isset($modes[$this->options->outputInterface])) {
            return;
        }

        $mode = $modes[$this->options->outputInterface];

        if ((imagetypes() & $mode) !== $mode) {
            throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputInterface));
        }

    }

    /**
     * {@inheritDoc}
     *
     * @throws \zxf\QrCode\Output\QRCodeOutputException
     */
    protected function prepareModuleValue(mixed $value): int
    {
        $values = [];

        foreach (array_values($value) as $i => $val) {

            if ($i > 2) {
                break;
            }

            $values[] = max(0, min(255, intval($val)));
        }

        $color = imagecolorallocate($this->image, ...$values);

        if ($color === false) {
            throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
        }

        return $color;
    }

    protected function getDefaultModuleValue(bool $isDark): int
    {
        return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
    }

    /**
     * {@inheritDoc}
     *
     * @throws \ErrorException|\zxf\QrCode\Output\QRCodeOutputException
     */
    public function dump(?string $file = null): string|GdImage
    {
        $this->image = $this->createImage();
        // set module values after image creation because we need the GdImage instance
        $this->setModuleValues();
        $this->setBgColor();

        if (imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background) === false) {
            throw new QRCodeOutputException('imagefilledrectangle() error');
        }

        $this->drawImage();

        if ($this->upscaled) {
            // scale down to the expected size
            $scaled = imagescale($this->image, ($this->length / 10), ($this->length / 10));

            if ($scaled === false) {
                throw new QRCodeOutputException('imagescale() error');
            }

            $this->image = $scaled;
            $this->upscaled = false;
            // Reset scaled and length values after rescaling image to prevent issues with subclasses that use the output from dump()
            $this->setMatrixDimensions();
        }

        // set transparency after scaling, otherwise it would be undone
        // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
        $this->setTransparencyColor();

        if ($this->options->returnResource) {
            return $this->image;
        }

        $imageData = $this->dumpImage();

        $this->saveToFile($imageData, $file);

        if ($this->options->outputBase64) {
            $imageData = $this->toBase64DataURI($imageData);
        }

        return $imageData;
    }

    /**
     * Creates a new GdImage resource and scales it if necessary
     *
     * we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
     *
     * @see https://github.com/chillerlan/php-qrcode/issues/23
     *
     * @throws \zxf\QrCode\Output\QRCodeOutputException
     */
    protected function createImage(): GdImage
    {

        if ($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20) {
            // increase the initial image size by 10
            $this->length *= 10;
            $this->scale *= 10;
            $this->upscaled = true;
        }

        $im = imagecreatetruecolor($this->length, $this->length);

        if ($im === false) {
            throw new QRCodeOutputException('imagecreatetruecolor() error');
        }

        return $im;
    }

    /**
     * Sets the background color
     */
    protected function setBgColor(): void
    {

        if (isset($this->background)) {
            return;
        }

        if ($this::moduleValueIsValid($this->options->bgColor)) {
            $this->background = $this->prepareModuleValue($this->options->bgColor);

            return;
        }

        $this->background = $this->prepareModuleValue([255, 255, 255]);
    }

    /**
     * Sets the transparency color, returns the identifier of the new transparent color
     */
    protected function setTransparencyColor(): int
    {

        if (! $this->options->imageTransparent) {
            return -1;
        }

        $transparencyColor = $this->background;

        if ($this::moduleValueIsValid($this->options->transparencyColor)) {
            $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
        }

        return imagecolortransparent($this->image, $transparencyColor);
    }

    /**
     * Returns the image quality value for the current GdImage output child class (defaults to -1 ... 100)
     */
    protected function getQuality(): int
    {
        return max(-1, min(100, $this->options->quality));
    }

    /**
     * Draws the QR image
     */
    protected function drawImage(): void
    {
        foreach ($this->matrix->getMatrix() as $y => $row) {
            foreach ($row as $x => $M_TYPE) {
                $this->module($x, $y, $M_TYPE);
            }
        }
    }

    /**
     * Creates a single QR pixel with the given settings
     */
    protected function module(int $x, int $y, int $M_TYPE): void
    {

        if (! $this->drawLightModules && ! $this->matrix->isDark($M_TYPE)) {
            return;
        }

        $color = $this->getModuleValue($M_TYPE);

        if ($this->drawCircularModules && ! $this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)) {
            imagefilledellipse(
                $this->image,
                (($x * $this->scale) + intdiv($this->scale, 2)),
                (($y * $this->scale) + intdiv($this->scale, 2)),
                (int) ($this->circleDiameter * $this->scale),
                (int) ($this->circleDiameter * $this->scale),
                $color,
            );

            return;
        }

        imagefilledrectangle(
            $this->image,
            ($x * $this->scale),
            ($y * $this->scale),
            (($x + 1) * $this->scale),
            (($y + 1) * $this->scale),
            $color,
        );
    }

    /**
     * Renders the image with the gdimage function for the desired output
     *
     * @see https://github.com/chillerlan/php-qrcode/issues/223
     */
    abstract protected function renderImage(): void;

    /**
     * Creates the final image by calling the desired GD output function
     *
     * @throws \zxf\QrCode\Output\QRCodeOutputException
     */
    protected function dumpImage(): string
    {
        ob_start();

        $this->renderImage();

        $imageData = ob_get_contents();

        if ($imageData === false) {
            throw new QRCodeOutputException('ob_get_contents() error');
        }

        imagedestroy($this->image);

        ob_end_clean();

        return $imageData;
    }
}
